use std::error::Error;
use std::fs;

/** # 主体逻辑运行函数
将主体逻辑( 例如业务逻辑 )从 main 中分离出去，这样 main 函数就保留主流程调用，非常简洁。
## 返回值：
通过 Result 类型来返回结果，其中：
- Ok：返回 ()，表示程序正常运行
- Err：返回一个 Box<dyn Error>，表示程序运行出错，用Error的特征对象（dyn）来表示错误类型，用来处理不同类型的错误。
*/
pub fn run(command: Command) -> Result<(), Box<dyn Error>> {
    // 读取文件内容，通过?操作符来处理错误，返回一个Box<dyn Error>
    let contents = fs::read_to_string(command.file_path)?;

    // 调用搜索函数查询关键字，并遍历搜索结果，打印每一行
    println!("搜索到的结果如下：");
    for line in search(command.query, &contents){
        println!("{line}");
    };

    Ok(())
}

/// # 指令结构体
/// - 用于存储命令行参数
/// - 通过标注生命周期，告诉编译器，这个结构体的生命周期与传入的参数列表的生命周期相同
pub struct Command<'a> {
    pub query: &'a str,
    pub file_path: &'a str,
}

/// 添加一个新的方法，用于构造 Command 结构体，并解析命令行参数
/// 由于该方法需要访问传入的参数列表，因此需要标注生命周期
impl<'a> Command<'a> {
    /** # 解析命令行参数
    传入的参数列表是一个字符串向量，其中包含了命令行参数。
    第一个参数是程序的名称，第二个参数是要搜索的关键字，第三个参数是要搜索的文件路径。
    这个函数会返回一个元组，包含要搜索的关键字和要搜索的文件路径。

    ## 参数：
    - args: Vec<String>，命令行参数列表

    ## 返回值：
    - Result<Self, &'static str>，如果解析成功，返回一个元组，包含要搜索的关键字和要搜索的文件路径；如果解析失败，返回一个错误信息
    - Ok：返回Self（当前结构体的实例）
    - Err：错误信息：
        - 如果参数列表长度小于3，返回一个错误信息，提示用户按照以下格式运行程序：cargo run -- [关键字] [文件路径]
        - &'static str: 静态字符串的引用，生命周期为'static，即程序运行期间一直存在
        - 错误信息在当前函数头部的判断中，通过return Err返回

    ## 运行方式：
    - cargo run -- 关键字 文件路径
    - 其中：-- 后面的内容会被当作参数传递给程序
    */
    pub fn build(args: &'a [String]) -> Result<Self, &'static str> {
        println!("\n获取到用户输入的参数如下：");
        dbg!(args);
        println!("\n\n接下来开始解析命令行参数：");

        // 检查参数列表长度是否小于3，如果是，返回错误信息
        if args.len() < 3 {
            return Err("指令错误，请按照以下格式运行程序：cargo run -- [关键字] [文件路径]");
        }

        let query = &args[1];
        let file_path = &args[2];

        Ok(Self { query, file_path })
    }
}

/** # 搜索函数
- 用于在文件内容中查找指定的关键字
- 返回一个字符串向量，其中包含所有匹配的行
*/
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new(); // 创建一个空的字符串向量，用于存储匹配的行
    // 遍历文件内容中的每一行
    // 这里使用了 lines 方法，它会返回一个迭代器，其中包含了文件内容中的每一行
    // 然后，我们使用 for 循环来遍历这个迭代器
    for line in contents.lines() {
        // 检查当前行是否包含指定的关键字
        // 这里使用了 contains 方法，它会返回一个布尔值，表示当前行是否包含指定的关键字
        if line.contains(query) {
            results.push(line); // 如果包含，将当前行添加到结果向量中
        }
    }

    // 返回结果向量
    results
}

/** # 测试模块
- 用于测试 search 函数
- 这里使用了 cfg(test) 宏，它会告诉编译器，这个模块只在测试环境中编译
- 这样，我们就可以在测试环境中使用这个模块，来测试 search 函数
*/
#[cfg(test)]
mod tests {
    use super::*;

    /** # 测试搜索函数
    - 用于测试 search 函数
    - 这里使用了 cfg(test) 宏，它会告诉编译器，这个模块只在测试环境中编译
    - 这样，我们就可以在测试环境中使用这个模块，来测试 search 函数
    */
    #[test]
    fn search_result() {
        // 定义一个用于查询的关键字
        let query = "duct";
        // 定义一个用于测试查询的文本内容
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        // 调用 search 函数进行查询，并断言查询结果是否符合预期
        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
